Skip to main content

03 · Plan-and-Execute:先谋后动

前置:01 ReAct 的 4.6 节(messages 如何增长)。本篇所有问题都源于那里。

一、问题:任务一长,Agent 就自己收工了

给 ReAct Agent 派活:「把项目里所有旧版日志库调用改成新版,改完跑测试。」

第 1 步   搜索           → 找到 12 处
第 2-8 步 逐个改 → 改完 6 个
第 9 步 第 7 个文件还依赖了旧版专有方法
第 10 步 查新版替代
第 11 步 修好这个文件
第 12 步 「已完成日志库迁移。」 ← 还剩 5 个文件,测试一次没跑

成因不是模型笨,是上下文结构。 每轮调模型都要重发完整历史,此时历史是:

位置内容
最前面「12 处 + 跑测试」这个全局目标
中段11 轮工具调用,上万 token 的文件内容
末尾刚修好第 7 个文件

模型注意力在长上下文中段最弱(lost in the middle),最清楚的是末尾。刚修完一个文件、感觉完整,就收工了。

ReAct 的结构缺陷:只有「下一步」,没有「全局」。

两个朴素解法都不够用:

  • 在 system prompt 里强调目标 —— 它是静态的,记不住「已经改了几个」
  • 让模型先列个计划 —— 方向对,但计划列完放哪?打印出来模型下轮看不见;塞进 system prompt 就改不了,而第 9 步恰好证明计划一定要改

所以这个范式真正要解决的是两件事:① 全局目标怎么始终停在模型看得见的地方;② 计划中途怎么改。

二、两条路线

① 静态计划② 动态 todo
计划何时定开头一次随时改
存在哪一个 Python 变量Agent 状态 + 上下文
谁维护你的编排代码模型自己,通过工具调用
代表Plan-and-Solve(arXiv 2305.04091)Claude Code TodoWrite、Manus todo.md、LangChain write_todos
适合步骤能提前想清的推理题边做边发现的工程任务

生产环境跑的基本都是② —— 真实任务的计划一定会变。但①值得先看,因为它把「执行器需要哪些信息」暴露得最清楚。

三、静态计划:执行器需要四样东西

Plan-and-Solve 范式的两阶段工作流

图 3-1 静态计划的两阶段

图片来源:Hello-Agents 第四章(CC BY-NC-SA 4.0)

规划器没什么可讲的 —— 一次普通调用,要它输出步骤列表。重点在执行器:执行第 3 步时要告诉模型什么?

EXECUTOR_PROMPT = """严格按计划解决"当前步骤",只输出该步答案。
# 原始问题: {question}
# 完整计划: {plan}
# 历史步骤与结果: {history}
# 当前步骤: {current_step}
"""

四个占位符,各对应一种翻车:

占位符不给会怎样
question忘了最终目标,把第 3 步当孤立小任务做
plan不知道当前步的位置,越界去干第 4 步
history拿不到前面算出的中间值,只能瞎猜
current_step试图一次把整个问题解完

si=πsolve(q, P, (s1,,si1))s_i = \pi_{\text{solve}}(q,\ P,\ (s_1, \dots, s_{i-1}))

两个死穴,正是路线②要解决的:

  1. history 全量累加 —— 每步都要带上之前所有步骤,步骤数翻倍则 token 翻四倍
  2. 计划改不了 —— 第 2 步发现原计划有误,只能硬走或整个重来

针对死穴 1 有两个变体:ReWOO 在规划期就用 #E1#E2 写清步骤间的变量依赖,执行时不带历史;LLM Compiler 把计划编译成 DAG 并行跑。两者都假设「计划不会变」,所以生产上少见。

四、动态 todo:把计划做成一个工具

不在 ReAct 外面套规划阶段,而是给 ReAct 加一个「写待办清单」的工具。改计划本身就是一次工具调用,于是计划随时可改、且以工具结果的形式进入消息历史。

4.1 数据结构:三个状态,故意缺一个

class Todo(TypedDict):
content: str
status: Literal["pending", "in_progress", "completed"]

没有优先级、ID、依赖、子任务,也没有 failed

failed 是刻意的。 有这个状态,模型遇到搞不定的任务就会标记 failed 然后跳过;而你要的是它去解决卡住的原因。官方规定:

遇到错误、阻塞或无法完成时保持 in_progress;被阻塞时新建一个任务描述需要解决什么。

用状态机里缺一个状态来约束模型行为。

4.2 34% 的文件是提示词

实测 langchain/agents/middleware/todo.py

字符数
整个文件15,525
工具描述3,881
系统提示词1,378
提示词占比34%

Python 逻辑不到 60 行,工具本体只有 10 行(todos 全量覆盖,不是增量更新)。

这是本篇的认知转折点0102 的行为由代码结构决定,Plan-and-Execute 的行为几乎全由提示词决定。删掉 write_todos 的描述,工具还在,模型不会用了。

4.3 提示词里真正改变行为的四条

① 反复劝你别用

如果用户的请求很简单、不到 3 步,最好不要用,直接做。

开头和结尾各说一遍。写清单要烧 token 和延迟,小任务上是纯负收益。

② 立刻标记,禁止批量标

批量标记会让上下文里长时间存在一份过期清单,模型看到「都还是 pending」可能重做已完成的事。

③ 永远至少有一个 in_progress

给模型一个「当前焦点」锚点。全是 pending 时模型不知道自己在哪。

④ 标记完成 ≠ 交付答案

write_todos 追踪工作,它不交付答案。用户要的东西必须出现在最后一次 write_todos 之后的消息里。

对应一个真实翻车场景:

第 9 轮  调 write_todos,全部标记 completed
第 10 轮 模型觉得没事干了,不调工具 → 循环退出 → 用户屏幕上什么都没有

因为 01 讲过,退出条件是「这轮没有工具调用」,而它最后一个动作恰好是调工具。

4.4 实测发现:清单其实没被回注上下文

wrap_model_call 钩子只往 system prompt 追加了使用说明,没有注入当前 todos 状态。全文件搜一遍:todos 只写不读。模型看到清单的唯一途径是历史里那条工具返回消息:

Updated todo list to [{'content': '搜索日志调用', 'status': 'completed'}, ...]

所以对话越长,清单越往中段沉 —— 正好落进第一节那个注意力低谷。

对比 Manus 的做法每完成一步就把 todo.md 重写一遍追加到上下文末尾。 表面在更新文件,实际是把全局目标反复搬到注意力最强的位置。

LangChainManus
todo 的定位给用户看的进度条对抗注意力衰减的工具
用户可见主要目的副作用

自己实现建议照 Manus 做,至少在每次调模型前把清单注入 system prompt 末尾。

五、故障排查

现象原因修法
列了清单然后就停了最后动作是调工具,循环判定结束4.3 ④
清单写得好但没照着做清单沉到上下文中段每轮回注末尾(4.4)
看个变量类型也列 5 条 todo缺劝阻性提示词明写「不到 3 步别用」
某任务永远 in_progress模型不知道卡住了该干嘛要求「被阻塞时新建解决阻塞的任务」
清单错乱互相覆盖一轮并行调了两次,全量覆盖竞态拦截并行调用,回错误让模型重试
第 10 步 prompt 三万 token静态计划全量 history换动态 todo,或摘要压缩

最后那条竞态,生产实现专门写了段代码拦,注释是:

清单被设计为每轮最多更新一次write_todos 每次替换整个清单,多次并行调用会产生「哪次优先」的歧义。

处理方式不是抛异常,是构造错误消息塞回历史,让模型看到「并行调用被拒」,下轮自己改成单次。给模型可读懂、可改正的错误 —— 这条会在 05、06 反复出现。

六、定位与边界

Plan-and-Execute 是在 ReAct 之上加一层全局视野,没有取代 ReAct —— 动态 todo 本身就跑在 ReAct 循环里。

不该用:任务不到 3 步(官方自己劝你别用);路径完全确定(那是 Workflow);探索型任务(目标都不清楚,硬列计划反而限制它);首字延迟敏感(静态路线的规划阶段是一次完整往返)。

一句话:这个范式的关键不是「把计划列出来」,而是**「让模型每轮都还能看见它」**。

参考资料

资料位置协议
生产实现(主拆)langchain/libs/langchain_v1/langchain/agents/middleware/todo.py,357 行MIT
教学实现s05_todo_write/code.py,13KBMIT
静态计划路线Hello-Agents 第四章 4.3CC BY-NC-SA 4.0
官方教程langgraph/examples/plan-and-execute/plan-and-execute.ipynbMIT
为什么 todo 有效Manus — Context Engineering for AI Agents——

Wang L, Xu W, Lan Y, et al. Plan-and-Solve Prompting. arXiv:2305.04091, 2023.

下一篇:04 · Reflection